02 IMX6ULL裸机开发:IMX6ULL启动流程解析

创建时间:2021/12/28 19:22
更新时间:2022/1/18 16:16
作者:gi51wa2j
标签:100ask_IMX6ULL_v11, bingo, 正文

一、IM6ULL程序启动相关知识简要

为使得方便理解,此处借03/04节LED 韦东山老师视频讲解中部分内容先行阐述,点击链接
led程序make过程截图
led.imx=头部+led.bin              (头部包含:被复制到哪里去)
led.img=1k.bin_led.imx

1.1 IMX6ULL程序简要讲解

ROM里面固化一个DDR,把程序读取到DDR中,头部就包含一些信息,长度和存放地址
emmc烧写用imx文件,而TF/SD卡烧写通常使用

1.2 嵌入式系统内部硬件组成(链接

XIP设备:eXecute In Place,本地执行,由ubootload直接调用

ARM板子支持多种启动方式:XIP设备启动、非XIP设备启动等等。

比如:Nor Flash、SD卡、SPI Flash,甚至支持UART、USB、网卡启动。

这些设备中,很多都不是XIP设备。


问:既然CPU无法直接运行非XIP设备的代码,为何可以从非XIP设备启动?

答:上电后,CPU运行的第1条指令、第1个程序,位于片内ROM中,它是XIP设备。这个程序会执行必要的初始化,

比如设置时钟、设置内存:再从”非XIP设备“中把程序读到内存:最后启动这上程序。


问:系统没有上电的时候,我们所写的程序存放在哪?

答:外部emmc(或tf卡)中,我们上电之后,CPU从片内ROM中执行第一条指令,读取emmc(或tf卡)中的程序到DDR中,然后从DDR中我们所设置的程序运行地址开始执行程序。


问题:系统支持SPI FLASH启动.SPI FLASH不是XIP设备,cpu无法直接执行里面的代码,CPU如何执行SPI FLASH上的代码?一上电,CPU执行的第1个程序、第1条指令在哪里?

答:在ROM中,他帮助CPU去运行SPI上面的程序,ROM的程序通常由C语言完成,但是其中变量存放在RAM区域 


(ROM如果可以直接写的话,又很大的概率会把om破坏掉,整个系统就废了,所以设置为只可读)

对于CPU外接的复杂外设,其芯片内都有对应的控制器,可以方便的的处理,CPU只需要专注逻辑运算即可;
无论ROM或RAM都太小,所以需要外接DDR;
最后需要一个总的memory controller 来控制CPU发出的地址所到达的位置;

猜测:ARM芯片内部有很多部件,这是一个片上系统(System on chip),

比如有:

cpu

rom

ram

memory controller --ddr

sd/mmc controller --sd card

spi controller

spi flash

usb controller

---

usb storage device

uart controller

interrtupt controller

二、IMX6ULL启动流程

2.1 IMX6ULL启动方式

参考资料:网盘开发板配套资料“06_Datasheet(数据手册)/Core_board/CPU/IMX6ULLRM.pdf”中《Chapter 8: System Boot》。

2.1.1 芯片手册讲解

IMX6ULL芯片内部有一个boot ROM,上电后boot ROM上的程序就会运行。它会根据BOOT_MODE[1:0]的值,以及eFUSE或GPIO的值决定后续的启动流程。

注:eFUSE即熔丝,只能烧写一次,一般正式发布产品时烧写最终值;平时调试时通过GPIO来设置开发板的启动方式。

boot ROM上的程序功能强大,可以从USB口或串口下载程序并把它烧写到Flash等设备上,也可以从SD卡或EMMC、Flash等设备上读出程序、运行程序。

 

问题来了:

① boot ROM是从USB口下载、运行程序,还是从SD卡等设备上读出、运行程序,谁决定?
由BOOT_MODE[1:0]的值来决定启动方式,它们来自于2个引脚BOOT_MODE1、BOOT_MODE0。这2个引脚在上电时是输入引脚,芯片启动后采集这2个引脚的值,存入BOOT_MODE寄存器。以后这2个引脚就可以用于其他功能,不会影响到BOOT_MODE寄存器。
BOOT_MODE[1:0]的值确定了4种启动模式,如下图:
在100ASK_IMX6ULL中,这2个引脚对应的原理图如下:

00模式在我们的开发过程中很少用到,简单介绍一下:在这种模式下,GPIO的值被忽略。Boot  ROM会根据eFUSE的值来选择启动设备、设置启动设备。但是,对于刚出厂的芯片eFUSE值可能是错乱的、不适合你的设备的,怎么办?eFUSE中有一个值BT_FUSE_SEL,它的出厂值是0,表示eFUSE未被烧写。boot ROM程序发现BT_FUSE_SEL为0时,它会通过USB或串口来下载程序;发现BT_FUSE_SEL为1时,才会根据eFUSE的值选择启动设备,读出、运行该设备上的程序。
01模式,boot ROM程序通过USB或串口下载、运行程序,这个模式可以用来烧写EMMC等设备。我们的开发板出厂时,就是通过这个模式下载、烧写出厂程序的。
10模式,称之为内部模式,简单地说就是从SD卡、EMMC等设备启动程序。这就引入下面第2个问题。
② 如何选择启动设备?
00模式下是通过eFUSE的值选择启动设备,我们不关心。
10模式下既可以通过eFUSE的值也可以通过GPIO的值来选择启动设备,但是到底通过谁来决定?eFUSE中有一个值BT_FUSE_SEL,对,又是它。它的初始值为0,表示eFUSE未被烧写。在10模式下,当BT_FUSE_SEL为0时就会通过GPIO来选择启动设备;当BT_FUSE_SEL为1时就会通过eFUSE来选择启动设备。
在开发阶段,我们使用GPIO来选择设备,这就引入下面第3个问题。

③ 如何通过eFUSE或GPIO选择、设置启动设备?
通过eFUSE或GPIO不仅能选择启动设备,还可以设置启动设备。
为什么还需要设置?比如Nand Flash参数各有不同,有些的页大小是2048,有些是4096。这些参数不同,boot ROM程序读Nand Flash的方法就不同,我们必须把这些参数告诉boot ROM:通过eFUSE或GPIO来标明这些参数。

首先看看要设置哪些eFUSE或GPIO来选择不同的启动设备。
从上图可知,既可以使用eFUSE也可以使用GPIO来选择启动设备,换句话说GPIO可以覆盖eFUSE的值。哪些GPIO覆盖哪些eFUSE?这可以查看IMX6ULL芯片手册《Chapter 8: System Boot》里的《GPIO boot overrides》,我们把它摘出来放在3.1.2小节里。

选择启动设备后,还需要标明一些参数。
比如选择EMMC启动时,EMMC接在哪一个接口,eSDHC1还是eSDHC2?它的速度如何?
比如选择TF卡启动时,TF卡接在哪一个接口,eSDHC1还是eSDHC2?它的速度如何?

假设使用EMMC启动,或是TF卡启动,怎么设置eFUSE或GPIO?这些信息可以查询IMX6ULL芯片手册《Chapter 5:  Fusemap》,摘录如下。
当BOOT_MODE设置为0b00时,通过eFUSE选择启动设备,通过eFUSE获得设备的参数。
当BOOT_MODE设置为0b10时,通过eFUSE或GPIO来选择启动设备,获得设备的参数;使用eFUSE还是GPIO由eFUSE中的BT_FUSE_SEL决定,它默认是0,表示使用GPIO。

BOOT_MODE为0b10为例,解析一下上图。
要设置为SD卡、TF卡启动,有2个设置方法:
a. 设置eFUSE的BOOT_CFG1[7:5]为0b010,或
b. 查看《8.3.2 GPIO boot overrides》确定BOOT_CFG1[7:5]对应的GPIO为LCD1_DATA07~05,把这3个引脚设置为0b010。

根据SD卡、TF卡的性能,可以设置eFUSE或GPIO来表示它能否提供更高的速度:
a. 设置eFUSE的BOOT_CFG1[4:0],或
b. 查看《8.3.2 GPIO boot overrides》确定BOOT_CFG1[4:0]对应的GPIO为LCD1_DATA04~00,设置这些引脚。

IMX6ULL有两个SD卡、TF卡接口,使用哪一个接口?请看下表
a. 设置eFUSE的BOOT_CFG2[4:3]可以确定使用eSDHC1或eSDHC2,或
b. 查看《8.3.2 GPIO boot overrides》确定BOOT_CFG2[4:3]对应的GPIO为LCD1_DATA12~11,设置这些引脚

通过eFUSE或GPIO,还可以标明启动设备的更多参数,具体细节可以参考芯片手册《Chapter  5:  Fusemap》,作为硬件开发人员需要去细细研究;作为软件开发人员,实际上只需要看开发板手册知道怎么设置启动开关即可。

2.1.2 100ASK_IMX6ULL启动方式选择

100ASK_IMX6ULL开发板上的红色拨码开关用来设置启动方式、选择启动设备,支持这3种方式:EMMC启动、SD卡启动、USB烧写
板子背后画有一个表格,表示这3种方式如何设置。
表格如下:
BOOT CFG
BOOT
SW1(LCD_DATA5)
SW2(LCD_DATA11)
SW3(BOOT_MODE0)
SW4(BOOT_MODE1)
EMMC
OFF
OFF
ON
OFF
SD
ON
ON
ON
OFF
USB
X
X
OFF
ON

拔码开关中的SW3、SW4用来设置BOOT_MODE,ON表示1,OFF表示0
所以当SW3、SW4设置为ON、OFF时,BOOT_MODE为0b10,将会使用SD卡、TF卡、EMMC等设备启动。
刚出厂的开发板中BT_FUSE_SEL默认为0,表示使用GPIO来设置参数。即使用LCD1_DATA07~05来选择启动设备。
100ASK_IMX6ULL开发板只支持SD/TF卡、EMMC启动,LCD1_DATA07~05为0b010时选择SD/TF卡启动,LCD1_DATA07~05为0b011时选择EMMC启动。这两种启动设备对应的LCD1_DATA07~06的值相同,都是0b01,这在核心板上已经通过电阻设置好,我们只需要在拨码开关上设置SW1(对应LCD1_DATA05)就可以。
IMX6ULL上有2个EMMC Flash接口,也复用为2个SD/TF卡接口,通过LCD1_DATA12~11来选择接口。0b00对应eSDHC1接口,0b01对应eSDHC2接口。LCD1_DATA12的值在核心板上已经通过电阻设置好。LCD1_DATA11的值通过拨码开关SW2来设置ON表示0,对应eSDHC1接口,100ASK_IMX6ULL的TF卡接口使用了eSDHC1接口OFF表示1,对应eSDHC2接口,100ASK_IMX6ULL的EMMC接口使用了eSDHC2接口

这3种启动方式的设置示意图如下:
要注意的是,设置为USB启动时,不能插上SD卡、TF卡。
刚出厂的板子在EMMC上烧写了系统,你可以设置为EMMC启动方式。

2.1.3 GPIO boot overrides

IMX6ULL中既可以通过eFUSE也可以通过GPIO来选择、设置启动设备,在手册里大部分场合只列出了eFUSE,对应的GPIO需要查表:IMX6ULL芯片手册《Chapter 8: System Boot》里的《GPIO  boot overrides》。
我们把它摘录出来。

2.2 IMX6ULL启动流程

这个启动流程可以猜测出来,假设板子设置为SD/TF卡启动,boot ROM程序会做什么?把程序从SD/TF卡读出来,运行。
从哪里读?从SD/TF卡读,这需要先初始化SD/TF卡:根据eFUSE或GPIO的设置初始化SD/TF卡。
读到哪里去?读到内存即DDR去,这需要先初始化DDR。
除了初始化启动设备、初始化DDR,还需要初始化什么?也许要初始化时钟,让CPU跑得更快一点。
总结起来就是:初始化CPU、时钟等,初始化内存,初始化启动设备,从启动设备上把程序读入内存,运行内存的程序。
官方的启动流程如下,这个流程图比较粗糙,总结起来就是:
a. 检查CPU ID
b. 检查Reset Type,冷启动、唤醒的启动过程是不一样的
c. 检查启动模式BOOT_MODE,检查eFUSE或GPIO
d. 根据上述检查从USB口、UART口或是某个启动设备下载boot image
e. 认证image
f. 启动

对于具体的启动设备,IMX6ULL芯片手册《Chapter 8: System Boot》中有对应章节描述更为细致的启动流程。基本上就是对这些启动设备根据eFUSE或GPIO的设置进行初始化,尝试更高的工作频率等。
在往后的学习中,如果涉及这些细节,我们再描述。

假设使用SD/TF卡启动,卡上的程序有多大?它应该被复制到DDR哪里去?这些问题,请看《1.3 IMX6ULL映像文件》。

2.3 IMX6ULL映像文件

2.3.1 格式概述

     如果您有S3C2440或其他单片机的学习经验,可以知道程序的二进制版本,比如lcd.bin可以直接烧写到Flash上。它们是自启动的,什么意思?比如一上电,运行的是lcd.bin前面的代码,它会初始化内存,把自己从Flash上复制到内存里去执行。请记住:自己把自己复制到内存。
     但是对于IMX6ULL,烧写在EMMC、SD/TF卡上的程序,并不能“自己复制自己”,是“别人把它复制到内存里”。一上电首先运行的是boot ROM上的程序,它从EMMC、SD/TF卡上把程序复制进内存里。
     所以:boot ROM程序需要知道从启动设备哪个位置读程序,读多大的程序,复制到哪里去。
     所以:
启动设备上,不能仅仅烧写bin文件,需要在添加额外的信息。
     还有一个问题,IMX6ULL的boot ROM程序可以把程序读到DDR里,那需要先初始化DDR。每种板子接的DDR可能不一样,boot ROM程序需要初始化这些不同的DDR。boot ROM从哪里得到这些不同的参数?
     还有,IMX6ULL支持各种启动设备,比如各种Nor Flash。为了通用,boot ROM程序将会使用最保守的参数,也就是最慢的时序来访问Nor Flash。为加快启动程序,boot ROM程序可以根据我们提供的信息初始化硬件,让它以更优的参数运行。
     这些参数信息,被称为“Device Configuration Data”,设备配置数据(DCD),这些DCD将会跟bin文件一起打包烧写在启动设备上。boot ROM程序会从启动设备上读出DCD数据,根据DCD来写对应的寄存器以便初始化芯片。DCD中列出的是对某些寄存器的读写操作,我们可以在DCD中设置DDR控制器的寄存器值,可以在DCD中使用更优的参数设置必需的硬件。这样boot ROM程序就会帮我们初始化DDR和其他硬件,然后才可以把bin程序读到DDR中并运行。
    总结起来,烧写在EMMC、SD卡或是TF卡上的 ,除了程序本身,还有位置信息、DCD信息,这些内容合并成一个映像文件,如下图:

     这4部分内容合并成为一个映像文件,烧写在EMMC、SD卡或TF卡等启动设备的某个固定地址,boot ROM程序去这个固定地址读出映像文件。启动设备不同,固定地址不同,如下图:

2.3.2 格式详解

     先贴出一张图,然后再细细讲解:

下面的讲解图中,列有C语言格式的结构体,这些结构体来源于U-boot的tools目录下的imximage.h。
(在第二篇中下载到ubuntu中,回到8.2.4节)
对于程序员,有时候看结构体可以更快地理解映像文件的格式。

(1). Image Vector Table(IVT):
     IVT会被放在固定的地址,IVT中是一系列的地址,boot ROM程序会根据这些地址来确定映像文件中其他部分在哪里。
IVT格式如下:
要注意的是上图中这4项:
a. header:
     里面有3项:tag、length、version。length表示IVT的大小,它是32字节。要注意是的,它是大字节序的。
b. entry:
     用户程序运行时第1条指令的地址,就是程序的链接地址、程序被复制到内存哪里。
c. dcd:
     映像被复制到内存后,其中的DCD数据的地址。
d. boot data:
     映像被复制到内存后,其中的boot data的地址。
e. self:
     映像被复制到内存后,IVT自己所在的地址。
(2). Boot data:
     映像被复制到内存后,整个映像文件(IVT之前还有几个扇区数据,比如分区表)所在的地址。

a. start:
     这是映像文件在内存中的地址,以SD/TF卡为例:
映像文件=(1K数据,内含分区表等信息)+IVT+BootData+DCD+用户数据(bin文件)
注意,IVT并不在映像文件的最前面,start也不是IVT在内存中的地址,而是整个映像文件在内存中的地址:
start = IVT在内存中的地址 - IVT offset
     什么意思?假设IVT被保存在启动设备TF卡1024偏移地址处,IVT被复制到内存地址0x87000000,那么start=0x87000000-1024。
所以start表示的是启动设备开头的数据,被复制到内存哪里去。
     从它的含义也可以推理出:boot ROM程序会把启动设备开头的数据,复制到内存;而不仅仅是从IVT开始复制。
b. length:
  保存在启动设备上的整个映像文件的长度,从0地址开始(不是从IVT开始)。
c. plugin:
     这是一个标记位,当它为1时表示这个映像文件是“plugin”,即插件
     boot ROM程序可以支持有限的启动设备,如果你想双持更多的启动设备比如网络启动、CDROM启动,就需要提供对应的驱动。这些驱动就是“plugin”,我们的教程不涉及,该标记位为0。
     Boot data就是用来表示映像文件应该被复制到哪里去,以前它的大小。boot ROM程序就是根据它来把整个映像文件复制到内存去的。
(3). DCD:
     DCD的作用在前面讲解过,简单地说就是设备的配置信息,里面保存有一些寄存器值。
实际上DCD还可以更复杂,它支持多种命令:write data、check data、nop、unlock。我们可以通过write data命令写寄存器,通过check data命令等待寄存器就绪。

DCD格式如下:

     DCD以Header开始,里面的TAG为0xD2表示它是DCD,里面还标明了DCD的大小、版本。
     接下来就是各个“CMD”,你可以在一个“CMD”里操作多个寄存器,比如在一个“write  data command”中,写多个寄存器。
以“write data command”为例简单介绍一下,它的格式为:

     上图中,TAG为0xCC表示这是“write data command”;Length表示命令的大小;Parameter的作用稍后再说。
既然是写命令,那自然就有“地址、值”,上图中就是多个“Address、Value/Mask”。为何还有Mask?这要结合Parameter来讲解:

     Parameter中b[2:0]用来表示写操作的字节数,是以字节、半字(2 byte),还是字(4  byte)来操作。
     而b[4]、b[3]决定了是写值(write value),清位(clear bitmask),还是设位(set  bitmask)。

     对于其他命令,共格式可以参考IMX6UL的芯片手册,这里就不再介绍了。

(4). User code and data:
     就是用户程序或数据,原原本本地添加到映像文件里就可以。

2.3.3 实例

    我们制作映像文件的目的什么?把我们自己的程序烧写到启动设备,让boot ROM程序启动它。
     所以制作映像文件的起点是:我们编写的程序。
     制作过程中各项值的计算方法如下图所示(我们当然不需要手工去计算,一个mkimage命令就搞定了)。
     上图中各步骤细说如下:
① 确定入口地址entry:
     我们的程序运行时要放在内存中哪一个位置,这是我们决定的。它被称为入口地址、链接地址。
② 确定映像文件在内存中的地址start:
    boot ROM程序启动时,会把“Initial Load Region”读出来,“Initial load  Region”里含有IVT、Boot data、          DCD。boot ROM根据DCD初始化设备后,再把整个映像文件读到内存。
     在启动设备上,“Initial Load Region”之后紧跟着我们的程序,反过来说就是我们程序的前面,放着“Initial Load Region”。假设“Initial Load Region”的大小为load_size,那么在内存中“Initial Load Region”的位置start = entry – load_size。
注意:“Initial Load Region”位于启动设备0位置,它的头部并不是IVT,而是一些无用的数据(或是分区信息)。
在IMX6ULL中有一个表格,列出了不同启动设备对应的“Initial Load Region  Size”:

③ 确定IVT在内存中的地址self:
     我们知道IVT在启动设备上某个固定的位置,上或中的“Image Vector Table  Offset”:ivt_offset。那么在内存中它的位置可以如下计算:
self = start + ivt_offset = entry – load_size + ivt_offset

④ 确定Boot data在内存中的地址boot_data:
     IVT的大小是32字节,IVT之后就是Boot data,而IVT中的boot_data值表示Boot data在内存中的位置,计算如下:
boot_data = self + 32 = entry – load_size + ivt_offset + 32

⑤ 确定DCD在内存中的地址dcd:
     Boot data的大小是12字节,Boot data之后就是DCD,而IVT中的dcd值表示DCD在内存中的位置,计算如下:
dcd = boot_data + 12 = entry – load_size + ivt_offset + 44

⑥ 写入DCD的数据:
     DCD是用初始化硬件的,特别是初始化DDR。而DDR的初始化非常的复杂、专业,我们一般是使用硬件厂家提供的代码。
     在后面的程序中你可以看到,我们是使用类似下面的指令来制作映象文件
./tools/mkimage -n ./tools/imximage.cfg.cfgtmp -T imximage -e 0x80100000 -d led.bin led.imx

     上述命令中的imximage.cfg.cfgtmp就是厂家提供的,内部截取部分贴出来:
     从上图也可以看到imximage.cfg.cfgtmp文件中基本是对寄存器的写操作。
   mkimage程序来自u-boot,它会把imximage.cfg.cfgtmp中的内容转换为DCD数据。我们并不打算讲解DCD的内容,只需要了解它的大概作用:

⑦ 写入用户程序
⑧ 经过上述7个步骤,整个映像文件就构造出来了,可以把它烧入启动设备。
     我们提供的示例程序4_led中有一个文件:led.img,它就是映象文件,可以直接烧入TF卡。用软件Hex Editor Neo打开led.img,选择doble word方式显示,可以看到如下内容,你可以自行验证一下映像文件中各个值。

2.3.4 补充:每一个指令的字节数

     每一个机器码都有其存放的地址,这个地址的大小取决于设备所使用的指令集是Thumb、ARM或者Thumb2。
例如使用ARM指令集是32位,那么每个操作指令占据四个字节的地址。